ThinkPHP5 Db类源码分析
一、 类库结构
ThinkPHP5 Db类是由Db .php(用户入口) , Connection (连接器) , Builder( SQL构造器 ),Query(查询器)四部分构成,用户对数据库的所有操作均通过Db .php完成
ThinkPHP5 Db类位于thinkphp\library\think目录,完整结构如下:
1 | think |
二、 模块分析
1. 用户入口 Db.php
用户入口是用户和数据库操作类的桥梁,作用和index.php入口文件类似,在这个类中,主要完成了调用驱动类和连接器Connection完成连接,然后通过连接器进行下一步操作
这里的用户是指Db类的使用方,可能是一段代码,不是应用程序的最终用户
connect
方法
1 | /** |
$name
变量以md5的形式作为同一配置文件的连接标识,当传入的$name
变量为True
或类中不存在$instance[$name]
属性时,将重新建立连接,并将数据库的连接保存在$instance[$name]
中,调用完成后,返回值为实例化数据库的连接。在实例化连接类的时候,是根据配置文件的要求实例化不同的数据库连接类。
__callStatic
方法
1 | /** |
理解这个方法的核心在于了解__callStatic()
和call_user_func_array()
这两个方法
__callStatic()
是php中的一个魔术方法,当静态调用类中不存在的方法时,__callStatic()
方法就会执行,同时将调用的方法名和参数分别作为两个参数传入__callStatic()
方法
call_user_func_array()
方法:调用回调函数,并把一个数组参数作为回调函数的参数,它不仅可以调用一个函数,同时还可以调用一个类方法,第一个参数传入一个数组,数组元素分别为类和方法名即可,分析connect
方法时我们已经知道该方法返回值时一个已经实例化的Connection`类
call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数
以下列操作为例,看看最终发生了什么
1 | Db::Query("SELECT * FROM tablename"); |
由于Db类中并没有Query
方法,因此执行了Db类的__callStatic
方法,call_user_func_array
方法实际是执行了Connection
类中的Query
方法(这里忽略驱动类,实际是现执行驱动类方法,驱动类继承了Connection
)
到了这一步,程序已经由Db.php进入到Connection
类中了,下面我们再来看看Connection
模块
2. 连接器 Connection
连接器 顾名思义 连接就是它的核心能力,连接器主要用于与数据库的交互,如创建数据库的连接,记录连接的相关信息,发送sql语句到数据库执行(Query模块的功能也是依赖连接器Connection的query和execute方法实现的),事务等功能,再ThinkPHP中,连接器由Connection基类和数据库驱动类共同组成
connect
方法
1 | /** |
connect
方法有三个默认参数$config , $linknum ,$autoConnection
分别为数据库连接配置信息,连接序号,以及分布式数据库是否连主库。如果$linkNum
不存在,将 实例化一个PDO对象连接到数据库,并将连接或者说PDO对象保存在$this->link[$linkNum]
中,connect
方法的返回值$this->links[$linkNum]
就是新创建或者已存在的连接对象
initConnect
方法
连接器在执行Query或execute方法时,均会调用初始化连接方法,该方法的核心在于将connect方法建立的PDO对象保存在linkID属性
1 | /** |
query
方法
1 | /** |
方法内部第一行 $this->initConnect($master);
作用在于判断是否需要连接主服务器,同时将数据库连接对象(PDO对象)保存在linkID
属性中,再将SQL语句保存在queryStr
属性中,如果需要进行参数绑定,再将绑定参数保存在bind
属性中。
$querryTimes
是Db类的一个静态属性,用于保存Db类的查询次数。
$this->PDOStatement = $this->linkID->prepare($sql)
使用PDO对象的prepare()
$^{注1}$方法对sql语句进行预处理并保存在PDOStatement
属性中。
$procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec'])
$^{注2} $ 通过去除左部空白字符,字符串截取及全部转换为小写等对sql字符串的处理,获取sql语句真正开始的4的字符,判断是否为call或exec 如果是,则为存储过程调用,对存储过程和非存储过程的调用使用不同的参数绑定方法,然后执行查询返回查询的结果集
注1:
PDO::prepare()
— 准备要执行的SQL语句并返回一个PDOStatement
对象(PHP 5 >= 5.1.0, PECL pdo >= 0.1.0)注2:
strtolower()
函数把字符串转换为小写
execute
方法
1 | /** |
execute
方法的功能和query
方法相似,略有不同,query方法在分布式数据库下,默认读取的时从服务器数据,可以设置读取主服务器数据,execute方法只能操作主服务器数据,query的返回值是查询的结果集,而execute方法的返回值则是操作影响的行数,如果通过execute方法执行查询语句,返回的将是查询到的结果行数。
query
方法更适合执行读取操作,即查询语句,而execute
方法更适合执行写入操作,即插入,更新,删除等
__call
方法
1 | /** |
连接器Connection调用查询器Query的方法,是通过__call
方法实现的,__call
也是PHP中的一个魔术方法,功能与__callStatic
类似
__call 该方法在调用的方法不存在时会自动调用
Connection 方法另一大功能是对事务的操作,这里先不展开分析,以后有机会再单独做深入分析
3. 查询器 Query
查询器侧重于对数据的操作。查询器对对数据进行一定的预处理,然后调用SQL生成器Builder生成SQL语句,再通过连接器Connection与数据库交互。ThinkPHP DB类的链式操作的主要方法都在Query类中实现,包括聚集函数,LIMIT GROUP BY ORDER BY等操作
调用流程
对查询器的分析 我们使用Db类进行查询的实例进行展开
1 | # 实例1 查询一条数据 |
Db::table('think_user')
Db类中并没有table方法,前面的分析中我们已经知道,Db类中有一个__callStatic
的魔术方法,实现在Db类中调用Connection类的方法,而Connection类中同样有一个__call
的魔术方法,实现对Query
类方法的调用。实际的执行过程是这样的
Db类中没有table方法,因此触发执行__callStatic方法
1
Db :: __callStatic("table",["think_user"])
__callStatic方法的 self::connect()实例化了 Connection
1
$conn = new Connection()
__callStatice方法内部的call_user_func_array方法要执行Connection的table方法
1
$conn -> table("think_user")
Connection方法中同样没有table方法,执行 __call魔术方法
1
$conn -> __call("table",['think_user'])
__call通过$this->getQuery() 实例化了Query
1
2$conn -> getQuery()
$query = new Query()__call 方法内部的call_user_func_array方法执行Query的table方法
1
$query -> table("think_user")
table方法指定当前要查询的数据表名,保存在Query对象 option[‘table’]属性中,然后返回$this即$query 这也是链式调用的实现方法
1
$query -> option['table'] = "think_user"
table方法执行完毕,继续执行where方法 ,where方法指定AND查询条件
1
$query -> where("id",1)
where方法内部使用
func_get_args
方法获取参数列表,保存在$param
变量中,where的第一个参数是字段名,使用array_shift
方法删除$param
的第一个元素
1 | $param = func_get_args(); |
- 使用
parseWhereExp
方法分析条件表达式
1 | $query -> parseWhereExp('AND', "id", 1, null, $param) |
parseWhereExp
方法对查询条件进行一系列的分析处理后,将查询条件保存到Query对象的option属性中,之后where方法同样返回$this
即Query对象
1 | $query -> options['multi']["AND"]["ID"] = ["eq",1] |
where
方法执行完毕,接下来执行find方法
1 | $query -> find() |
- 在find方法内部,调用parseExpress方法分析条件表达式获取条件参数保存到
$options
变量
1 | $options = $query -> parseExpress() |
- 调用buider属性也就是Buider对象的select方法组装select SQL
1 | $sql = $query -> bulider -> select($options) |
- 调用query方法执行查询
1 | $query -> query($sql) |
到这里整个查询结果就基本结束了,当然实际的情况比上述描述还要复杂,比如parseWhereExp方法内部的处理过程,这里只是选择最核心的路线进行分析
parseWhereExp
方法分析
先看看parseWhereExp
方法的源码
1 | /** |
很长,直接看源码比较难受,我们以官网的三种示例来逐一进行分析:
表达式查询:
新版的表达式查询采用全新的方式,查询表达式的使用格式:
1
2
3
4
5 > Db::table('think_user')
> ->where('id','>',1)
> ->where('name','thinkphp')
> ->select();
>
更多的表达式查询语法,可以参考查询语法部分。
以上部分节选自 ThinkPHP5 完全开发手册
在上方所列代码中,出现了两种表达式 "id",">",1
和"name","thinkphp"
这两种表达式等价于id>1
和name='thinkphp'
那么parseWhereExp
接收到这样的条件表达式,是如何进行处理的呢?
首先看where方法在调用parseWhereExp方法时传入了什么参数
1 | public function where($field, $op = null, $condition = null) |
下面再来看parseWhereExp方法内部的执行过程
1 | # 将logic 全部转为大写 |
看似很长的方法,在执行 $this->parseWhereExp('AND', "id", ">", 1, ["id", ">", 1])
时 实际有效执行的只有以下代码
1 | $logic = "AND", |
注:
preg_match
方法preg_match 函数用于执行一个正则表达式匹配。
语法:
1
2 > int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )
>
搜索 subject 与 pattern 给定的正则表达式的一个匹配。
参数说明:
- $pattern: 要搜索的模式,字符串形式。
- $subject: 输入字符串。
- $matches: 如果提供了参数matches,它将被填充为搜索结果。 $matches[0]将包含完整模式匹配到的文本, $matches[1] 将包含第一个捕获子组匹配到的文本,以此类推。
- $flags:flags 可以被设置为以下标记值:
- PREG_OFFSET_CAPTURE: 如果传递了这个标记,对于每一个出现的匹配返回时会附加字符串偏移量(相对于目标字符串的)。 注意:这会改变填充到matches参数的数组,使其每个元素成为一个由 第0个元素是匹配到的字符串,第1个元素是该匹配字符串 在目标字符串subject中的偏移量。
- offset: 通常,搜索从目标字符串的开始位置开始。可选参数 offset 用于 指定从目标字符串的某个未知开始搜索(单位是字节)。
返回值
返回 pattern 的匹配次数。 它的值将是 0 次(不匹配)或 1 次,因为 preg_match() 在第一次匹配后 将会停止搜索。preg_match_all() 不同于此,它会一直搜索subject 直到到达结尾。 如果发生错误preg_match()返回 FALSE。